/*
 * Copyright 2008 Georgi Staykov
 * 
 * This file is part of pscoder.
 *
 * pscoder is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * pscoder is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with pscoder.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.gstaykov.pscoder.editor.completion;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;

import com.gstaykov.pscoder.Activator;
import com.gstaykov.pscoder.preferences.PreferenceConstants;
import com.gstaykov.pscoder.util.Logger;
import com.gstaykov.pscoder.util.Util;

public class CompletionDictionary {
	private static final String DICT_FILENAME_SUFFIX = "Dictionary.dic";
	private static final String[] GET_ASSEMBLIES_CMD = new String[] {"powershell.exe", "-Command", "\"(Get-PSSnapin) | select AssemblyName | foreach {write-host $_.AssemblyName}\""};
	private static final String CMDLET_FINDER_FILENAME = "CmdletFinder.exe";
	public static final String PS_CMDLET_FILENAME = "CmdletFile";
    public static final String CUSTOM_CMDLET_FILENAME = "CustomCmdletFile";
	private static final String POWERSHELL_FILE_SUFFIX = ".ps1";
	private static final int READ_CHARS = 1024;
	private static HashMap<String, CompletionDictionary> instances = new HashMap<String, CompletionDictionary>();
	private static ResourceChangeListener changeListener = null;
	
	private IProject project = null;
	private String projectName = null;
	private IWorkspace workspace = null;
	private IWorkspaceRoot workspaceRoot = null;
	private Pattern srcFilesP = Pattern.compile("\\.\\s*([A-Za-z0-9\\\\/-]*\\.ps1)"); // FIXME: [IN] Should not match commented imports
	private Pattern functionsP = Pattern.compile("function \\s*([a-zA-Z0-9]*\\(.*\\))");
	private Pattern variablesP = Pattern.compile("\\$(script|global):[a-zA-z0-9_]*");
	private Logger logger = new Logger();
	
	private HashMap<String, FileData> dict = new HashMap<String, FileData>();
	
	private CompletionDictionary(String projName) throws CoreException {
	    this.projectName = projName;
		workspace = ResourcesPlugin.getWorkspace();
		workspaceRoot = workspace.getRoot();
		project = workspaceRoot.getProject(projName);
		if (project.exists()) {
			if (!project.isOpen()) project.open(null);
			if (dict.size() == 0) {
			    loadDictionary();
			}
		}
	}
	
	private void loadDictionary() throws CoreException {
	    File dictFile = new File(projectName + DICT_FILENAME_SUFFIX);
	    if (!dictFile.exists()) {
	        generateDictionary();
	    } else {
	        try {
	            BufferedReader in = new BufferedReader(new FileReader(dictFile));
	            int files = Integer.parseInt(in.readLine());
	            for (int i = 0; i < files; i++) {
	                FileData data = new FileData(in.readLine());
	                data.setSourcedFiles(getDictFileNextBlock(in));
	                data.setFunctions(getDictFileNextBlock(in));
	                data.setVariables(getDictFileNextBlock(in));
	                dict.put(data.fileName, data);
	            }
	        } catch (Exception e) {
	            logger.logError("", e);
	        }
	    }
	}
	
	private String[] getDictFileNextBlock(BufferedReader in) throws IOException {
	    int linesCount = Integer.parseInt(in.readLine());
	    String[] lines = new String[linesCount];
	    for (int i = 0; i < linesCount; i++) lines[i] = in.readLine();
	    return lines;
	}
	
	private void generateDictionary() throws CoreException {
		IPath projectRootPath = project.getLocation();
		File projectRoot = projectRootPath.toFile();
		addSourceDirectoryToDict(projectRoot);
		
		IResource[] members = project.members();
		for (IResource member : members) {
		    if (member.isLinked()) {
		        addSourceDirectoryToDict(member.getLocation().toFile());
		    }
		}
		
		addKnownCmdlets();
		
		saveDictionary();
	}
	
	private void addKnownCmdlets() {
	    try {
	        // First add powershell native cmdlets
	        ArrayList<String> assemblies = new ArrayList<String>();
	        Process proc = Runtime.getRuntime().exec(GET_ASSEMBLIES_CMD);
	        proc.getOutputStream().close();
	        BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
	        String newLine = null;
	        while ((newLine = reader.readLine()) != null) {
	            assemblies.add(newLine);
	        }
	        reader.close();
	        
	        ArrayList<String> cmdlets = new ArrayList<String>();
	        for (String assembly : assemblies) {
	            proc = Runtime.getRuntime().exec(new String[] {CMDLET_FINDER_FILENAME, assembly});
	            reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
	            newLine = null;
	            while ((newLine = reader.readLine()) != null) {
	                cmdlets.add(getCmdletString(newLine));
	            }
	            reader.close();
	        }
	        FileData data = new FileData(PS_CMDLET_FILENAME);
            data.setFunctions(cmdlets.toArray(new String[cmdlets.size()]));
            dict.put(PS_CMDLET_FILENAME, data);
	        
            // Now add custom snapins
            cmdlets.clear();
	        String[] customFiles = parsePSSnapinFiles(Activator.getDefault().getPluginPreferences().getString(PreferenceConstants.PS_SNAPIN_FILES));
	        for (int i = 0; i < customFiles.length; i++) {
                proc = Runtime.getRuntime().exec(new String[] {CMDLET_FINDER_FILENAME, customFiles[i]});
                reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
                newLine = null;
                while ((newLine = reader.readLine()) != null) {
                    cmdlets.add(getCmdletString(newLine));
                }
                reader.close();
	        }
	        
            data = new FileData(CUSTOM_CMDLET_FILENAME);
            data.setFunctions(cmdlets.toArray(new String[cmdlets.size()]));
            dict.put(CUSTOM_CMDLET_FILENAME, data);
	    } catch (Exception e) {
	        logger.logError("An error occured while adding known commandlets", e);
	    }
	}
	
	private String getCmdletString(String cmdlet) {
	    StringBuffer sb = new StringBuffer();
	    StringTokenizer st = new StringTokenizer(cmdlet, ",");
	    if (st.hasMoreTokens()) {
	        sb.append(st.nextToken());
	    }
	    
	    while (st.hasMoreTokens()) {
	        sb.append(" -" + st.nextToken());
	    }
	    
	    return sb.toString();
	}
	
	// FIXME: This does exactly the same job as FileEditor.parseString - make utility method
	private String[] parsePSSnapinFiles(String fileList) {
        StringTokenizer st = new StringTokenizer(fileList, PreferenceConstants.SNAPIN_FILES_SEPARATOR);
        ArrayList<String> elements = new ArrayList<String>();
        while (st.hasMoreTokens()) {
            String token = st.nextToken();
            if (token.length() != 0) {
                elements.add(token);
            }
        }
        return elements.toArray(new String[elements.size()]);
	}
	
	private void addSourceDirectoryToDict(File directory) {
		String[] files = directory.list();
		for (int i = 0; i < files.length; i++) {
			File file = new File(directory.getAbsolutePath() + File.separatorChar + files[i]);
			if (file.isDirectory()) {
				addSourceDirectoryToDict(file);
			}
			
			addFileToDict(file);
		}
	}
	
	private void addFileToDict(File file) {
        if (file.getName().endsWith(POWERSHELL_FILE_SUFFIX)) {
            String fileRelPath = file.getAbsolutePath(); 
            FileData data = new FileData(fileRelPath);
            
            String text = loadFile(file);
            data.setSourcedFiles(getSourcedFiles(text));
            data.setFunctions(getFunctions(text));
            data.setVariables(getVariables(text));
            dict.put(fileRelPath, data);
        }
	}
	
	private String[] getSourcedFiles(String content) {
		Set<String> sourced = new HashSet<String>();
		
		Matcher matcher = srcFilesP.matcher(content);
		while(matcher.find()) {
		    sourced.add(matcher.group(1));
		}
		
		String[] result = new String[sourced.size()];
		return sourced.toArray(result);
	}
	
	private String[] getFunctions(String content) {
        Set<String> sourced = new HashSet<String>();
        
        Matcher matcher = functionsP.matcher(content);
        while(matcher.find()) {
            sourced.add(matcher.group(1));
        }
        
        String[] result = new String[sourced.size()];
        return sourced.toArray(result);
	    
	}
	
	private String[] getVariables(String content) {
        Set<String> sourced = new HashSet<String>();
        
        Matcher matcher = variablesP.matcher(content);
        while(matcher.find()) {
            sourced.add(matcher.group(0));
        }
        
        String[] result = new String[sourced.size()];
        return sourced.toArray(result);
	}
	
	private String loadFile(File file) {
		StringBuffer sb = new StringBuffer();
		char[] buffer = new char[READ_CHARS];
		
		try {
			BufferedReader br = new BufferedReader(new FileReader(file));
			int readChar = br.read(buffer);
			while (readChar != -1) {
				sb.append(buffer, 0, readChar);
				readChar = br.read(buffer);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return sb.toString();
	}
	
	private void saveDictionary() {
	    File dictFile = new File(projectName + DICT_FILENAME_SUFFIX);
	    try {
	        PrintWriter out = new PrintWriter(dictFile);

	        Set<String> keys = dict.keySet();
	        out.println(keys.size());
	        
	        for (String key : keys) {
	            FileData data = dict.get(key);
	            out.println(data.fileName);
	            
	            // write included files
	            String[] incFiles = data.getSourcedFiles();
	            out.println(incFiles.length);
	            for (int i = 0; i < incFiles.length; i++) {
	                out.println(incFiles[i]);
	            }

                // write functions
                String[] functions = data.getFunctions();
                out.println(functions.length);
                for (int i = 0; i < functions.length; i++) {
                    out.println(functions[i]);
                }

                // write variables
                String[] variables = data.getVariables();
                out.println(variables.length);
                for (int i = 0; i < variables.length; i++) {
                    out.println(variables[i]);
                }
	        }
	        
	        out.close();
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}
	
	public static CompletionDictionary getInstance(String project) throws CoreException {
	    CompletionDictionary result = CompletionDictionary.instances.get(project);
	    if (result == null) {
	        result = new CompletionDictionary(project);
	        CompletionDictionary.instances.put(project, result);
	    }
	    
	    if (changeListener == null) {
	        // Install a resource change listener which should update the dictionaries if a workspace resource is changed
	        changeListener = new ResourceChangeListener();
	        ResourcesPlugin.getWorkspace().addResourceChangeListener(changeListener, IResourceChangeEvent.POST_CHANGE);
	    }
	    
	    return result;
	}
	
	public String getFullFilenameForEnding(String ending) {
		String lowerCasedEnding = Util.convertFileNameToWindowsNotation(ending).toLowerCase();
		String result = ending;

		// FIXME: HACK NOT CORRECT AND THE IMPLEMENTATION IS UGLY
        Set<String> keys = dict.keySet();
        for (String key : keys) {
            if (key.toLowerCase().endsWith(lowerCasedEnding)) {
                result = key;
                break;
            }
        }

        return result;
	}
	
	public HashMap<String, ICompletionProposal> getProposals(String prefix, String document, int offset) {
	    HashMap<String, ICompletionProposal> proposals = new HashMap<String, ICompletionProposal>();
	    
	    // FIXME: HACK NOT CORRECT AND THE IMPLEMENTATION IS UGLY
	    Set<String> keys = dict.keySet();
	    for (String key : keys) {
	        if (key.toLowerCase().endsWith(document.toLowerCase())) {
	            document = key;
	            break;
	        }
	    }
        // HACK MIGHT NOT BE CORRECT
	    
	    FileData data = dict.get(document);
	    if (data == null) {
	        // No proposals for this document
	        return proposals;
	    }
	    
	    String fileNameIncluded = document.substring(document.lastIndexOf('\\') + 1);
	    String[] variables = data.getVariables();
	    for (int i = 0; i < variables.length; i++) {
	        if (variables[i].startsWith(prefix)) {
	            String displayString = variables[i] + " - " + fileNameIncluded;
	            String stringToAdd = variables[i].substring(prefix.length());
	            CompletionProposal prop = new CompletionProposal(stringToAdd, offset, 0, stringToAdd.length(), null, displayString, null, null);
	            proposals.put(displayString, prop);
	        }
	    }
	    
       String[] functions = data.getFunctions();
       for (int i = 0; i < functions.length; i++) {
           String funcName = "";
           if (functions[i].indexOf('(') != -1) {
               // Add regular functions
               funcName = functions[i].substring(0, functions[i].indexOf('('));
           } else if (functions[i].indexOf(' ') != -1) {
               // Add cmdlets
               funcName = functions[i].substring(0, functions[i].indexOf(' '));
           } else {
               // Nothing to add
               break;
           }
           
           if (funcName.startsWith(prefix)) {
               String displayString = functions[i] + " - " + fileNameIncluded;
               String stringToAdd = funcName.substring(prefix.length());
               CompletionProposal prop = new CompletionProposal(stringToAdd, offset, 0, stringToAdd.length(), null, displayString, null, null);
               proposals.put(displayString, prop);
           }
       }

       String[] includes = data.getSourcedFiles();
       for (int i = 0; i < includes.length; i++) {
           proposals.putAll(getProposals(prefix, includes[i], offset));
       }
       
       return proposals;
	}
	
	public void loadFileChanges(File file) {
	    addFileToDict(file);
	    
	    // FIXME: For now every time a file is changed save the new dictionary
	    saveDictionary();
	}
	
	public void rebuildDictionary() throws CoreException {
	    File dictFile = new File(projectName + DICT_FILENAME_SUFFIX);
	    if (dictFile.exists()) {
	        dictFile.delete();
	    }
	    
	    dict = new HashMap<String, FileData>();
	    
        generateDictionary();
	}
	
	public FileData getFileData(String filename) {
	    return dict.get(filename);
	}
}
